#![allow(non_snake_case)]
use crate::gbmode::{GBMode, GBSpeed};
use crate::gpu::GPU;
use crate::keypad::Keypad;
use crate::serial::{Serial, SerialFn};
use crate::sound::sound::Sound;
use crate::timer::Timer;
use crate::{mbc, StrResult};

const WRAM_SIZE: usize = 0x8000;
const ZRAM_SIZE: usize = 0x7F;

#[derive(PartialEq)]
enum DMAType {
    NoDMA,
    GDMA,
    HDMA,
}

pub(crate) struct MMU<'a> {
    wram: [u8; WRAM_SIZE],
    zram: [u8; ZRAM_SIZE],
    hdma: [u8; 4],

    hdma_status: DMAType,
    hdma_src: u16,
    hdma_dst: u16,
    hdma_len: u8,
    wrambank: usize,
    gbspeed: GBSpeed,
    speed_switch_req: bool,
    // 0xFF72, 0xFF73, 0xFF75
    undocumented_cgb_regs: [u8; 3],

    // public
    pub inte: u8,
    pub intf: u8,
    pub serial: Serial<'a>,
    pub timer: Timer,

    pub keypad: Keypad,
    pub gpu: GPU,
    pub sound: Option<Sound>,
    pub mbc: Box<dyn crate::mbc::interface::IMbc>,
    pub gbmode: GBMode,
}

impl<'a> MMU<'a> {
    pub(crate) fn new(
        cart: Box<dyn mbc::interface::IMbc + 'static>,
        serialFn: Option<SerialFn<'a>>,
    ) -> StrResult<Self> {
        let serial = match serialFn {
            Some(cb) => Serial::new_with_callback(cb),
            None => Serial::new(),
        };

        let mut res = Self {
            wram: [0; WRAM_SIZE],
            zram: [0; ZRAM_SIZE],
            hdma: [0; 4],
            wrambank: 1,
            inte: 0,
            intf: 0,
            serial: serial,
            timer: Timer::new(),
            keypad: Keypad::new(),
            gpu: GPU::new(),
            sound: None,
            mbc: cart,
            gbmode: GBMode::Classic,
            gbspeed: GBSpeed::Single,
            speed_switch_req: false,
            hdma_src: 0,
            hdma_dst: 0,
            hdma_status: DMAType::NoDMA,
            hdma_len: 0xFF,
            undocumented_cgb_regs: [0; 3],
        };
        fill_random(&mut res.wram, 42);
        if res.rb(0x0143) == 0xC0 {
            return Err("this game does not work in classic mode");
        }
        res.set_initial();
        Ok(res)
    }

    pub(crate) fn new_cgb(
        cart: Box<dyn mbc::interface::IMbc + 'static>,
        serialFn: Option<SerialFn<'a>>,
    ) -> StrResult<Self> {
        let serial = match serialFn {
            Some(cb) => Serial::new_with_callback(cb),
            None => Serial::new(),
        };

        let mut res = Self {
            wram: [0; WRAM_SIZE],
            zram: [0; ZRAM_SIZE],
            wrambank: 1,
            hdma: [0; 4],
            inte: 0,
            intf: 0,
            serial: serial,
            timer: Timer::new(),
            keypad: Keypad::new(),
            gpu: GPU::new_cgb(),
            sound: None,
            mbc: cart,
            gbmode: GBMode::Color,
            gbspeed: GBSpeed::Single,
            speed_switch_req: false,
            hdma_src: 0,
            hdma_dst: 0,
            hdma_status: DMAType::NoDMA,
            hdma_len: 0xFF,
            undocumented_cgb_regs: [0; 3],
        };

        fill_random(&mut res.wram, 42);
        res.determine_mode();
        res.set_initial();
        Ok(res)
    }

    fn determine_mode(&mut self) {
        let mode = match self.rb(0x0143) & 0x80 {
            0x80 => GBMode::Color,
            _ => GBMode::ColorAsClassic,
        };

        self.gbmode = mode;
        self.gpu.gbmode = mode;
    }
    pub(crate) fn do_cycle(&mut self, ticks: u32) -> u32 {
        let cpudivider = self.gbspeed as u32;
        let vramticks = self.perform_vramdma();
        let gputicks = ticks / cpudivider + vramticks;
        let cputicks = ticks + vramticks * cpudivider;

        self.timer.do_cycle(cputicks);
        self.intf |= self.timer.interrupt;
        self.timer.interrupt = 0;

        self.intf |= self.keypad.interrupt;
        self.keypad.interrupt = 0;

        self.gpu.do_cycle(gputicks);
        self.intf |= self.gpu.interrupt;
        self.gpu.interrupt = 0;

        let _ = self.sound.as_mut().map_or((), |s| s.do_cycle(gputicks));

        self.intf |= self.serial.interrupt;
        self.serial.interrupt = 0;

        return gputicks;
    }
    pub(crate) fn rb(&mut self, addr: u16) -> u8 {
        match addr {
            0x0000..=0x7FFF => self.mbc.readrom(addr),
            0x8000..=0x9FFF => self.gpu.rb(addr),
            0xA000..=0xBFFF => self.mbc.readram(addr),
            0xC000..=0xCFFF | 0xE000..=0xEFFF => self.wram[addr as usize & 0x0FFF],
            0xD000..=0xDFFF | 0xF000..=0xFDFF => {
                self.wram[(self.wrambank * 0x1000) | addr as usize & 0x0FFF]
            }
            0xFE00..=0xFE9F => self.gpu.rb(addr),
            0xFF00 => self.keypad.rb(),
            0xFF01..=0xFF02 => self.serial.rb(addr),
            0xFF04..=0xFF07 => self.timer.rb(addr),
            0xFF0F => self.intf | 0b1110_0000,
            0xFF10..=0xFF3F => self.sound.as_mut().map_or(0xFF, |s| s.rb(addr)),
            0xFF4D | 0xFF4F | 0xFF51..=0xFF55 | 0xFF6C | 0xFF70
                if self.gbmode != GBMode::Classic =>
            {
                0xFF
            }

            0xFF72..=0xFF73 | 0xFF75..=0xFF77 if self.gbmode == GBMode::Classic => 0xFF,
            0xFF4D => {
                0b0111_1110
                    | (if self.gbspeed == GBSpeed::Double {
                        0x80
                    } else {
                        0
                    })
                    | (if self.speed_switch_req { 1 } else { 0 })
            }
            0xFF40..=0xFF4F => self.gpu.rb(addr),
            0xFF51..=0xFF55 => self.hdma_read(addr),
            0xFF68..=0xFF6B => self.gpu.rb(addr),

            0xFF70 => self.wrambank as u8,
            0xFF72..=0xFF73 => self.undocumented_cgb_regs[addr as usize - 0xFF72],

            0xFF75 => self.undocumented_cgb_regs[2] | 0b1000_1111,
            0xFF76..=0xFF77 => 0x00,
            0xFF80..=0xFFFE => self.zram[addr as usize & 0x007F],
            0xFFFF => self.inte,

            _ => 0xFF,
        }
    }

    fn set_initial(&mut self) {
        self.wb(0xFF05, 0);
        self.wb(0xFF06, 0);
        self.wb(0xFF07, 0);
        self.wb(0xFF10, 0x80);
        self.wb(0xFF11, 0xBF);
        self.wb(0xFF12, 0xF3);
        self.wb(0xFF14, 0xBF);
        self.wb(0xFF16, 0x3F);
        self.wb(0xFF16, 0x3F);
        self.wb(0xFF17, 0);
        self.wb(0xFF19, 0xBF);
        self.wb(0xFF1A, 0x7F);
        self.wb(0xFF1B, 0xFF);
        self.wb(0xFF1C, 0x9F);
        self.wb(0xFF1E, 0xFF);
        self.wb(0xFF20, 0xFF);
        self.wb(0xFF21, 0);
        self.wb(0xFF22, 0);
        self.wb(0xFF23, 0xBF);
        self.wb(0xFF24, 0x77);
        self.wb(0xFF25, 0xF3);
        self.wb(0xFF26, 0xF3);
        self.wb(0xFF40, 0x91);
        self.wb(0xFF42, 0);
        self.wb(0xFF43, 0);
        self.wb(0xFF45, 0);
        self.wb(0xFF47, 0xFC);
        self.wb(0xFF48, 0xFF);
        self.wb(0xFF49, 0xFF);
        self.wb(0xFF4A, 0);
        self.wb(0xFF4B, 0);
    }

    pub(crate) fn rw(&mut self, address: u16) -> u16 {
        (self.rb(address) as u16) | ((self.rb(address + 1) as u16) << 8)
    }

    pub(crate) fn ww(&mut self, addr: u16, value: u16) {
        self.wb(addr, (value & 0xFF) as u8);
        self.wb(addr + 1, (value >> 8) as u8);
    }

    pub(crate) fn wb(&mut self, addr: u16, value: u8) {
        match addr {
            0x0000..=0x7FFF => self.mbc.writerom(addr, value),
            0x8000..=0x9FFF => self.gpu.wb(addr, value),
            0xA000..=0xBFFF => self.mbc.writeram(addr, value),
            0xC000..=0xCFFF | 0xE000..=0xEFFF => self.wram[addr as usize & 0x0FFF] = value,

            0xD000..=0xDFFF | 0xF000..=0xFDFF => {
                self.wram[(self.wrambank * 0x1000) | (addr as usize & 0x0FFF)] = value
            }

            0xFE00..=0xFE9F => self.gpu.wb(addr, value),
            0xFF00 => self.keypad.wb(value),
            0xFF01..=0xFF02 => self.serial.wb(addr, value),
            0xFF04..=0xFF07 => self.timer.wb(addr, value),
            0xFF10..=0xFF3F => self.sound.as_mut().map_or((), |s| s.wb(addr, value)),
            0xFF46 => self.oamdma(value),
            0xFF4D | 0xFF4F | 0xFF51..=0xFF55 | 0xFF6C | 0xFF70 | 0xFF76..=0xFF77
                if self.gbmode != GBMode::Color => {}

            0xFF72..=0xFF73 | 0xFF75..=0xFF77 if self.gbmode == GBMode::Classic => {}
            0xFF4D => {
                if value & 0x1 == 0x1 {
                    self.speed_switch_req = true;
                }
            }

            0xFF40..=0xFF4F => self.gpu.wb(addr, value),
            0xFF51..=0xFF55 => self.hdma_write(addr, value),
            0xFF68..=0xFF68 => self.gpu.wb(addr, value),
            0xFF0F => self.intf = value,
            0xFF70 => {
                self.wrambank = match value & 0x7 {
                    0 => 1,
                    n => n as usize,
                };
            }

            0xFF72..=0xFF73 => self.undocumented_cgb_regs[addr as usize - 0xFF72] = value,

            0xFF75 => self.undocumented_cgb_regs[2] = value,

            0xFF80..=0xFFEE => self.zram[addr as usize & 0x007F] = value,
            0xFFFF => self.inte = value,

            _ => {}
        }
    }

    pub(crate) fn switch_speed(&mut self) {
        if self.speed_switch_req {
            if self.gbspeed == GBSpeed::Double {
                self.gbspeed = GBSpeed::Single;
            } else {
                self.gbspeed = GBSpeed::Double;
            }
        }

        self.speed_switch_req = false;
    }

    fn oamdma(&mut self, value: u8) {
        let base = (value as u16) << 8;
        for i in 0..0xA0 {
            let b = self.rb(base + i);
            self.wb(0xFE00 + i, b);
        }
    }

    fn hdma_read(&self, addr: u16) -> u8 {
        match addr {
            0xFF51..=0xFF54 => self.hdma[(addr - 0xFF51) as usize],

            0xFF55 => {
                self.hdma_len
                    | if self.hdma_status == DMAType::NoDMA {
                        0x80
                    } else {
                        0
                    }
            }

            // TODO: should use panic ?
            _ => panic!(
                "the address {:04X} should not be handled by hdma_read()",
                addr
            ),
        }
    }

    fn hdma_write(&mut self, addr: u16, value: u8) {
        match addr {
            0xFF51 => self.hdma[0] = value,
            0xFF52 => self.hdma[1] = value & 0xF0,
            0xFF53 => self.hdma[2] = value & 0x1F,
            0xFF54 => self.hdma[3] = value & 0xF0,
            0xFF55 => {
                if self.hdma_status == DMAType::HDMA {
                    if value & 0x80 == 0 {
                        self.hdma_status = DMAType::NoDMA;
                        return;
                    }
                }

                let src = ((self.hdma[0] as u16) << 8) | (self.hdma[1] as u16);
                let dst = ((self.hdma[2] as u16) << 8) | (self.hdma[3] as u16) | 0x8000;

                if !(src <= 0x7FF0 || (src >= 0xA000 && src <= 0xDFF0)) {
                    panic!("HDMA transfer with illegal start address {:04X}", src);
                }

                self.hdma_src = src;
                self.hdma_dst = dst;

                self.hdma_len = value & 0x7F;

                self.hdma_status = if value & 0x80 == 0x80 {
                    DMAType::HDMA
                } else {
                    DMAType::GDMA
                };
            }

            _ => panic!(
                "the address {:04X} should not be handled by hdma_write()",
                addr
            ),
        }
    }

    fn perform_vramdma(&mut self) -> u32 {
        match self.hdma_status {
            DMAType::NoDMA => 0,
            DMAType::GDMA => self.perform_gdma(),
            DMAType::HDMA => self.perform_hdma(),
        }
    }

    fn perform_hdma(&mut self) -> u32 {
        if self.gpu.may_hdma() == false {
            return 0;
        }

        self.perform_vramdma_row();
        if self.hdma_len == 0x7f {
            self.hdma_status = DMAType::NoDMA;
        }

        return 8;
    }

    fn perform_gdma(&mut self) -> u32 {
        let len = self.hdma_len as u32 + 1;
        for _ in 0..len {
            self.perform_vramdma_row();
        }

        self.hdma_status = DMAType::NoDMA;
        return len * 8;
    }

    fn perform_vramdma_row(&mut self) {
        let mmu_src = self.hdma_src;
        for j in 0..0x10 {
            let b = self.rb(mmu_src + j);
            self.gpu.wb(self.hdma_dst + j, b);
        }
        self.hdma_src += 0x10;
        self.hdma_dst += 0x10;

        if self.hdma_len == 0 {
            self.hdma_len = 0x7F;
        } else {
            self.hdma_len -= 1;
        }
    }
}

fn fill_random(slice: &mut [u8], start: u32) {
    // simple LCG to generate (non-cryptographic) random values
    // each distinct invocation should use a different start value
    const A: u32 = 110_351_524_5;
    const C: u32 = 123_45;
    let mut x = start;
    for v in slice.iter_mut() {
        x = x.wrapping_mul(A).wrapping_add(C);
        *v = ((x >> 23) & 0xFF) as u8;
    }
}
